iOS Responder Chain

Hit-Testing Returns the View Where a Touch Occurred

iOS 使用 hit-testing 找到具体的视图来响应点击。 Hit-testing实现检查了点击手势是落在了哪个视图区域。如果找到了视图,会递归遍历检查该视图下的所有子视图。最终会找到 位于视图层次最靠前(最靠近屏幕)的视图。iOS将其确定为hit-test view,将由它来消费这次touch事件。

Figure 2-1  Hit-testing returns the subview that was touched

更形象点的例子。我们结合Figure 2-1图片来看

1. 当点击手势位于view A。 我们要检查子视图view B 和view C
2. 点击手势没有位于view B,所以我们开始检查view C的子视图view D 和view E
3. 点击手势没有位于view D,所以点击手势被view E响应

hitTest:withEvent:CGPointUIEvent参数找到对应的hit test view

The hitTest:withEvent: method returns the hit test view for a given CGPoint and UIEvent. The hitTest:withEvent: method begins by calling the pointInside:withEvent: method on itself. If the point passed into hitTest:withEvent: is inside the bounds of the view, pointInside:withEvent: returns YES. Then, the method recursively calls hitTest:withEvent: on every subview that returns YES.

If the point passed into hitTest:withEvent: is not inside the bounds of the view, the first call to the pointInside:withEvent: method returns NO, the point is ignored, and hitTest:withEvent: returns nil. If a subview returns NO, that whole branch of the view hierarchy is ignored, because if the touch did not occur in that subview, it also did not occur in any of that subview’s subviews. This means that any point in a subview that is outside of its superview can’t receive touch events because the touch point has to be within the bounds of the superview and the subview. This can occur if the subview’s clipsToBounds property is set to NO.

Note: A touch object is associated with its hit-test view for its lifetime, even if the touch later moves outside the view.
The hit-test view is given the first opportunity to handle a touch event. If the hit-test view cannot handle an event, the event travels up that view’s chain of responders as described in The Responder Chain Is Made Up of Responder Objects until the system finds an object that can handle it.

The Responder Chain Is Made Up of Responder Objects

我们再聊聊如果事件没有被消费掉怎么办?

答案是: 事件会跟随响应链一级一级向上传递。最终给到 Application(继承自UIResponer)。

一个响应者对象(UIApplication/UIViewController/UIView)是能够响应和处理事件的。

第一响应者被设计为能第一个接受事件。要成为第一响应者,需要做以下两件事。

1. 重写 canBecomeFirstResponder 返回YES
2. 处理 becomeFirstResponder 调用。 必要情况可以主动调用这个方法

提示: becomeFirstResponder调用时机最好 在视图都准备到位之后。比如不要在viewWillAppear阶段。应该在viewDidAppear阶段在调用

响应者能处理的事件有:

* 触摸事件(Touch Events): 如果`hit test view`没有消费这个事件。事件按照响应链被向上传递
* 运动事件(Motion Events): UIKit处理这个事件,第一响应者必须要实现` motionBegan:withEvent:` 或 `motionEnded:withEvent:`方法
* 遥控事件(Remote control events): 第一响应者必须实现` remoteControlReceivedWithEvent:`方法
* Action messages: 比如按钮啊、开关啊。如果接受者的action为nil,也会按照响应链向上传递事件
* Editing-menu messages.  
* Text editing. 用户点击输入框或文本视图,UIKit默认自动将其置为第一响应者。接着,模拟键盘被唤起,光标聚焦。

只有Text FieldText View在用户点击之后,UIKit会自动将其置为第一响应者。其他的需要主动的调用becomeFirstResponder方法

响应链向上传递事件是有一些规则的。 看看以下图片的两种情况:

iOS_responder_chain

左图:

1. initial view不处理事件/消息,传递给父级view
2. 父级view接受到事件,不处理的话接着传递给父级view(viewController's view)
3. 父级view(viewController's view)接受到事件,不处理的话传递给viewController
4. viewController接收到事件,不处理传递给windows
5. windows接受到事件,不处理传递给 Application
6. 如果Application最后再不处理的话,事件就被忽略了。。

其中传递链路上任一响应者处理了事件,就中断传递了

右图:

1. initial view不处理事件/消息,传递给父级view(viewController's view)
2. 父级view接受到事件,传递给viewController
3. viewController接受到事件,传递给父级view
    步骤1-3 重复直到找到rootViewController
4. rootViewController 传递给 windows
5. windows 传递给 Application

参考链接